Fastapi conversion#2071
Conversation
There was a problem hiding this comment.
Removing jupyter and local datadog reference
There was a problem hiding this comment.
Removing jupyter and local datadog reference
There was a problem hiding this comment.
Fastapi uses uvicorn
There was a problem hiding this comment.
Convenient command runner
There was a problem hiding this comment.
Switch from pip building to uv.
!! Needs further verification
There was a problem hiding this comment.
Not using MongoEngine.
!! Still need to implement unit checks, but will be outside of this scope
There was a problem hiding this comment.
Upgrade python to 3.14; move to uv, ruff, and basedpyright; removed outdated/unnecessary packages
There was a problem hiding this comment.
the base stack is still at python 3.11
There was a problem hiding this comment.
Yeah, I have a note in the Dockerfile about requiring a 3.14 image build - I assumed it would be a good time to bump our python version and use up to date package versions.
Do you think this should be in the scope of the rewrite?
There was a problem hiding this comment.
getting to 3.12 would be a good start for the base stack
8dade26 to
fa4886c
Compare
There was a problem hiding this comment.
A lot of this logic is used to handle parsing dotted paths (data.x.y.z)
| wget -q https://github.com/materialsproject/MPContribs/blob/master/mpcontribs-api/mpcontribs/api/contributions/formulae.json.gz?raw=true \ | ||
| -O mpcontribs/api/contributions/formulae.json.gz |
There was a problem hiding this comment.
reason for removing the formulae file?
There was a problem hiding this comment.
The file seemed to just function as an id-to-formula translation. I made formulas required when POSTing.
If we want that moved back to optional and filled in automatically, I think I'd prefer actively fetching the formula over keeping it in a data file that we track in git. That would mean an extra query to mongo, and reaching into the nextgen database, but I feel that it is more reactive and resilient that way.
| FROM materialsproject/devops:python-3.1113.4 AS base | ||
| RUN apt-get update && apt-get install -y --no-install-recommends supervisor libopenblas-dev libpq-dev vim && apt-get clean | ||
| # FROM materialsproject/devops:python-3.1113.4 AS base | ||
| FROM python:3.14-slim AS base |
There was a problem hiding this comment.
I moved off of the shared base image here. There are definitely both pros and cons (below), and I have to test a full build still.
Pros:
- UV uses a lockfile and doesn't need to rely on manually finding and building binaries - this seems to be the main benefit of the shared image (scipy, matplotlib, etc.)
- Allows each portion of the stack to upgrade python independently (we don't have to hold back one piece until we build a shared image and force every upgrade at once)
Cons:
- If we have truly shared logic, we will have duplication, or revert to a shared image
- If the underlying default image changes, we would silently change as well
There was a problem hiding this comment.
image size is the core reason to start from the shared base image
| elif isinstance(value, list): | ||
| for item in value: | ||
| _validate_nested_keys(item) | ||
|
|
There was a problem hiding this comment.
Left as separate function in case we want to expand the validation to multiple steps.
tsmathis
left a comment
There was a problem hiding this comment.
Frankly this PR is too big to give a meaningful review across the entire thing without having some hands on experience. I roughly "get it", but I'll likely have a better handle on things once we go through trying to deploy this.
One overall thing that isn't quite clear to me is how I would extend/consume the underlying framework you have to start replacing the emmet-api. What would I need to do to replace the summary endpoint for example?
Is there some "core abstraction" running through this that I/we could build from to replace emmet-api's internals?
| from mpcontribs_api.domains._shared.models import Component, ComponentIn, DocumentOut | ||
| from mpcontribs_api.domains._shared.types import MD5Hash, PolarsFrame | ||
| from mpcontribs_api.projection import SparseFieldsModel | ||
|
|
There was a problem hiding this comment.
pretty much everything in this file will have a naming collision with a pymatgen class
should just reuse them
| class StructurePatch(SparseFieldsModel): | ||
| name: str | None = None | ||
| lattice: Lattice | None = None | ||
| sites: list[Site] | None = None | ||
| charge: float | None = None | ||
| cif: str | None = None |
There was a problem hiding this comment.
patching individual parts of a structure should be a no-go
There was a problem hiding this comment.
Didn't know that, but makes sense!
Added model_validator requiring all of {lattice, sites, charge, cif} be specified in a patch if one of them is. Additionally, I only allow charge to be None
| # AuthedDep = Annotated[User, Depends(require_user)] | ||
|
|
||
|
|
||
| # def require_role(role: str): | ||
| # def checker(user: AuthedDep) -> User: | ||
| # if not user.has_role(role): | ||
| # raise PermissionError(required_role=role) | ||
| # return user | ||
|
|
||
| # return Annotated[User, Depends(checker)] |
There was a problem hiding this comment.
These aren't actually being used. Removed them.
We handle role authorization at the data level, not the route level, so we end up working directly with the user object (which can tell us directly whether it can work with an object) rather than require_role. AuthedDep is not needed by how I constructed the use of User objects in repos and services. I construct a repo/service with a user at request time, so it is immediately scoped to the user - the route doesn't need to user object, just the repo/service.
| FROM materialsproject/devops:python-3.1113.4 AS base | ||
| RUN apt-get update && apt-get install -y --no-install-recommends supervisor libopenblas-dev libpq-dev vim && apt-get clean | ||
| # FROM materialsproject/devops:python-3.1113.4 AS base | ||
| FROM python:3.14-slim AS base |
There was a problem hiding this comment.
image size is the core reason to start from the shared base image
There was a problem hiding this comment.
getting to 3.12 would be a good start for the base stack
Yeah, it's quite large... For merging, it would be primarily the server architecture. We would make a "shared kernel" package of the "base" files (app, authz, dependencies, etc.) and make them more general (ie. parameterize the app factory). Then include the Resource.routers into the new app framework, use improved error handling, and switch our logging. We would also consolidate our data access logic with repositories (one class for MPContribs, one Read-Only class for emmet), pagination, and use fastapi-filter. Sharing the domain logic (our ODM, repos, services, models) is more difficult by nature. emmet is many read-only collections, so it makes sense to have factories/registries to handle efficient creation, while MPContribs collections have different behaviors. The core abstraction is Router -> Filter -> (service) -> Repository(scope, pagination) -> SparseFieldsModel (projects/ is a good example). We'd shift emmet like: QueryOperators -> Filter, SparseFieldsQuery -> SparseFieldsModel, GlobalHeaderProcessor -> scope, PaginationQuery -> Page+CursorParams. This isn't directly possible today - we need to define a ReadOnlyRepository that doesn't use Beanie and to create the "shared kernel". Addressing the Summary endpoint: most of the declaration would live in SummaryFilter (we'd include all fields, and most likely have to generate the __gte, __lte, etc. fields), and header_processor and query_to_configure_on_request would be removed in favor of scope. I'd also most likely want to move towards a simpler registry pattern - ideally just define a Filter and add a line to the registry. |
yep this would be great to work towards
Intuition says that at least the bones of the api logic should/will be sharable, and then yes the individual 'front ends' (emmet/contribs) would have to handle defining their respective models, etc.
any particular reason we can't use Beanie for everything that touches mongo? |
Definitely can be, but it depends how much boilerplate we want to manage. Ie. Adding a folder that contains a router.py, repo.py, and models.py for a few GET routes (no POST/PUT/DELETE/PATCH) that utilize a single model - not complex, but we could *probably* get the same done with fewer LOC by generating in a similar way to how we do it now.
100% can. It would unify how we interact with models, just again turns into a tradeoff of the extra boilerplate being worth it. It might end up reducing code elsewhere, so could be a win regardless. |
…ng a non-existent location
…verwrite the requested fields and add in null values for the non-requested fields
…from the number of documents sharing (project, identifier) in projects where unique_identifiers=False, otherwise version === 1
…e so ContributionIn does not need it.
…nd only allows charge to be None. Added tests for it
346d4c9 to
1b72f07
Compare
Summary
Start of major rewrite of the API server to FastAPI and other modern frameworks (pydantic, Beanie, etc.)
This PR is the essential groundwork of the rewrite. However, further PRs are necessary for full functionality.
Major TODOs after this PR
Minor TODOs after this PR: